# forward-compat boilerplate
from __future__ import absolute_import
from __future__ import with_statement
__metaclass__ = type

import os
import hmac
import hashlib

from fenton import util


DEFAULT_TTL = 600 # seconds
NONCELEN = 8
MACLEN = 20


def initialize(app):
    BuiltinUser.config = app.config


def get_builtin(username, password):
    builtins = (BuiltinUser.config.get('security.builtin.users') or '').split()
    if username not in builtins:
        return None

    pw = BuiltinUser.config.get('security.builtin.%s.password' % username) or ''
    if password is not None and not streq(password, pw):
        raise BadPassword

    return BuiltinUser(username=username)


def sha1impl(secret, nonce):
    if isinstance(secret, unicode):
        secret = secret.encode('UTF-8')
    return hashlib.sha1(secret + nonce).digest()


def secure_impl(secret, nonce):
    return make_key(secret, nonce, size=20, rounds=1000)


def streq(left, right):
    return sum(l==r for (l,r) in zip(left, right)) == len(left)


def cmphash(secret, stored, impl=sha1impl):
    nonce = stored.decode('base64')[20:]
    hashed = mkhash(secret, nonce, impl)
    fmt = '%%-%ds' % len(stored)
    hashed = fmt % hashed
    return streq(hashed, stored)


def mkhash(secret, nonce=None, impl=sha1impl):
    if nonce is None:
        nonce = os.urandom(NONCELEN)
    return (impl(secret, nonce) + nonce).encode('base64').strip()


# functions for integrating with windows NTLM


def _hmac(secret, msg):
    return hmac.new(secret, msg, hashlib.sha1).digest()


def encode_message(secret, msg, nonce=None):
    if nonce is None:
        nonce = os.urandom(NONCELEN)
    return util.enc64(_hmac(secret, nonce+msg) + nonce + msg)
    return _hmac(secret, nonce+msg) + nonce + msg


def decode_message(secret, string, noncelen=NONCELEN):
    # string is MAC(20) + nonce(noncelen) + msg
    string = util.dec64(string, '+/')
    mac = string[:MACLEN]
    nonce = string[MACLEN:MACLEN+noncelen]
    msg = string[MACLEN+noncelen:]

    if streq(mac, _hmac(secret, nonce+msg)):
        return msg
    return None


def pretend_ntlm(secret, itoken, user=None, noncelen=NONCELEN):

    ibytes = util.dec64(itoken)
    mac = ibytes[:MACLEN]
    macmsg = ibytes[MACLEN:]

    if not streq(mac, _hmac(secret, macmsg)):
        return None

    nonce = ibytes[MACLEN:MACLEN+noncelen]
    url = ibytes[MACLEN+noncelen:]

    if user is None:
        import getpass
        user = getpass.getuser()

    return util.enc64(_hmac(secret, nonce+user) + nonce + user)

    # dim macobj as new HMACSHA1(enc.GetBytes(secret))
    # dim itoken = Request.Parameter('_').replace('-', '+')
    # itoken = System.Convert.FromBase64String(itoken)

    # dim nonce as left(itoken, noncelen)
    # dim enc as new UTF8Encoding
    # dim msg = enc.GetBytes(nonce & user)
    # dim mac = macobj.ComputeHash(msg)

    # dim ret() as Byte
    # redim ret(mac.Length + msg.Length - 1)

    # System.Buffer.BlockCopy(mac, 0, ret, 0, mac.Length)
    # System.Buffer.BlockCopy(msg, 0, ret, mac.Length, msg.Length)

    # otoken = System.Convert.ToBase64String(ret)




def check(priv, context, obj=None):
    if priv in (True, False, None):
        result = priv
    elif isinstance(priv, basestring):
        result = Dynamic(priv).check(context, obj)
    else:
        result = priv(context, obj)
    return bool(result)



def get_crypter(key, impl=None, noncelen=32):
    return Crypter(key, impl or _default_impl, noncelen)


def get_signer(key, hasher=hashlib.sha1):
    return Signer(key, hasher)


def make_key(password, salt, rounds=1, size=32):
    from fenton import PBKDF2
    return PBKDF2.PBKDF2(password, salt, rounds).read(size)


class Priv:
    def __call__(self, context, obj=None):
        return self.check(context, obj)
    def __and__(self, other):
        return AND((self, other))
    def __or__(self, other):
        return OR((self, other))
    def __invert__(self):
        return NOT(self)


class _AuthenticatedPriv(Priv):
    def __repr__(self):
        return '<AUTHENTICATED>'
    def check(self, context, obj):
        return context.user.verify_authenticated(context)


class _RestrictedPriv(Priv):
    def __repr__(self):
        return '<RESTRICTED>'
    def check(self, context, obj):
        context.user.verify_authenticated(context)
        supers = context.app.config.get('security.superusers')
        return supers and context.user.has_privilege(supers)


class OR(Priv):
    def __init__(self, operands):
        assert operands
        self.operands = operands
    def check(self, context, obj):
        return any(f(context, obj) for f in self.operands)
    def __or__(self, other):
        return OR(self.operands + (other,))
    def __repr__(self):
        return ' | '.join('(%r)' % x  for x in self.operands)


class AND(Priv):
    def __init__(self, operands):
        assert operands
        self.operands = operands
    def check(self, context, obj):
        return all(check(f, context, obj) for f in self.operands)
    def __and__(self, other):
        return AND(self.operands + (other,))
    def __repr__(self):
        return ' & '.join('(%r)' % x  for x in self.operands)


class NOT(Priv):
    def __init__(self, operand):
        self.operand = operand
    def check(self, context, obj):
        return not self.operand(context, obj)
    def __invert__(self):
        return self.operand
    def __repr__(self):
        return '!%r' % self.operand


class Static(Priv):
    def __init__(self, privilege):
        self.privilege = privilege
    def check(self, context, obj):
        context.user.verify_authenticated(context)
        return context.user.has_privilege(self.privilege)
    def __repr__(self):
        return '~%s' % self.privilege


class Dynamic(Priv):
    def __init__(self, relation):
        self.relation = relation
    def check(self, context, obj):
        context.user.verify_authenticated(context)
        if obj is None:
            raise RecheckWithObject
        f = getattr(obj, self.relation, None)
        return f and f()
    def __repr__(self):
        return ':%s' % self.relation


class RecheckWithObject(Exception):
    pass


class InvalidSignature(Exception):
    pass


class SecurityException(Exception):
    def __unicode__(self):
        return unicode(str(self))
    def __str__(self):
        msg = util.decamel(self.__class__.__name__, sep=' ').capitalize()
        if self.args:
            msg += ': ' + str(self.args[0])
        return msg


class LoginFailed(SecurityException): pass
class NoUser(LoginFailed): pass
class BadUser(LoginFailed): pass
class BadPassword(LoginFailed): pass
class PoorPassword(LoginFailed): pass
class NotAllowed(SecurityException): pass
class NotAuthenticated(SecurityException): pass
class SessionExpired(NotAuthenticated): pass




class User:
    name = property(lambda x:x.username)
    groups = None
    authenticated = True
    password_stamp = None
    password_ttl = None
    password_age = None

    def __init__(self, username):
        self.username = username

    def __repr__(self):
        return '%s(%s)' % (self.__class__.__name__, self.username)

    def authenticate(self, request):
        raise NotImplementedError

    def unauthenticate(self, context):
        raise NotImplementedError

    def verify_authenticated(self, context):
        raise NotImplementedError

    def get_privileges(self):
        return self.groups

    def has_privilege(self, *required):
        has = self.get_privileges()
        return bool(has and (set(required) & has))


class AnonymousUser(User):
    name = 'Anonymous'
    username = '<anonymous>'
    authenticated = False
    __instance = None
    def __init__(self):
        pass
    def __repr__(self):
        return self.username
    def __new__(Class):
        if not Class.__instance:
            Class.__instance = User.__new__(Class)
        return Class.__instance
    def unauthenticate(self, context):
        return True
    def has_privilege(self, *required):
        raise NotAuthenticated
    def verify_authenticated(self, context):
        raise NotAuthenticated


class SystemUser(User):
    name = 'System'
    def authenticate(self, request):
        return True
    def unauthenticate(self, context):
        return True
    def verify_authenticated(self, context):
        return True
    def has_privilege(self, *required):
        return True


class BuiltinUser(User):
    password_ttl = None
    config = {}
    def __init__(self, username):
        self.username = username
    def authenticate(self, request):
        return True
    def unauthenticate(self, context):
        return True
    def verify_authenticated(self, context):
        return True

    @property
    def groups(self):
        default = (self.config.get('security.builtin.groups.default') or '').split()
        mine = (self.config.get('security.builtin.%s.groups' % self.username) or '').split()
        return set(default) | set(mine)


class RemoteUser(User):
    pass



ALLOWED = PUBLIC = True
DENIED = PRIVATE = False
AUTHENTICATED = _AuthenticatedPriv()
RESTRICTED = _RestrictedPriv()
ANONYMOUS = AnonymousUser()
SYSTEM = SystemUser('<system>')

_crypto_impl = {}
_default_impl = 'pycrypto_aes'

try:
    from pycryptopp.cipher.aes import AES as aes_cpp
except ImportError:
    pass
else:
    def do_cryptopp(key, data):
        return aes_cpp(key).process(data)
    _crypto_impl['cryptopp_aes'] = (do_cryptopp, do_cryptopp)


try:
    from Crypto.Cipher import AES
except ImportError:
    pass
else:
    def enc_aes(key, data):
        return AES.new(key, AES.MODE_CFB).encrypt(data)
    def dec_aes(key, data):
        return AES.new(key, AES.MODE_CFB).decrypt(data)
    _crypto_impl['pycrypto_aes'] = (enc_aes, dec_aes)


try:
    from Crypto.Cipher import Blowfish
except ImportError:
    pass
else:
    def enc_blowfish(key, data):
        return Blowfish.new(key, Blowfish.MODE_CFB).encrypt(data)
    def dec_blowfish(key, data):
        return Blowfish.new(key, Blowfish.MODE_CFB).decrypt(data)
    _crypto_impl['pycrypto_blowfish'] = (enc_blowfish, dec_blowfish)




class Crypter:
    def __init__(self, key, impl, noncelen=32):
        self.__key = key
        self.noncelen = noncelen
        try:
            self._enc, self._dec = _crypto_impl[impl]
        except KeyError:
            msg = 'algo %s not found, available algos: %s'
            raise Exception(msg % (impl, _crypto_impl.keys()))

    def key(self, nonce):
        return make_key(self.__key, nonce)

    def encrypt(self, data):
        nonce = os.urandom(self.noncelen)
        key = self.key(nonce)
        return nonce + self._enc(key, data)

    def decrypt(self, data):
        nonce = data[:self.noncelen]
        data = data[self.noncelen:]
        key = self.key(nonce)
        return self._dec(key, data)




class Signer:
    def __init__(self, key, hasher=hashlib.sha256):
        self.__key = key
        self.hasher = hasher
        self.hashlen = len(hasher('').digest())

    def verify(self, val):
        sig = val[:self.hashlen]
        val = val[self.hashlen:]
        expect = hmac.new(self.__key, val, self.hasher).digest()
        if not streq(expect, sig):
            raise InvalidSignature
        return val

    def sign(self, val):
        sig = hmac.new(self.__key, val, self.hasher).digest()
        return sig + val




